モザイクプロット#

モザイクプロットMosaic Plot ) とは、複数の質的変数に対して、その内訳を 長方形の面積 で表した可視化手法です。 その見た目から マリメッコプロットMarimekko Plot) とも呼ばれます。 分割方法を工夫することで三変数以上にも対応可能ですが,よく見かけるのは二変数に対する描画です。

二変数に対するモザイクプロットは、 積上げ棒グラフ の棒の太さを分母の大きさで調整したものと捉えることができます。 これにより、二変数を跨いだ(他の棒中の要素との)比較が可能になりますが、そもそも目視で面積を測るのは難しいため数値を付記すると親切です。

モザイクプロットは扱いが非常に難しい可視化手法です。 X軸の間隔が一定ではなくなってしまうため、変化率に関して誤った印象を与えてしまう可能性があります。

上図の年代別のマンガ雑誌のマンガ作者数シェアを表したモザイクプロットを例に説明します。 モザイクプロットは、 一つ目の 位置 スケール(上図「位置①」)で一つ目の質的変数(上図「年代」)の水準(上図「2000」年代)を指定し、 スケールで二つ目の質的変数の水準(上図「週刊少年ジャンプ」)を指定します。 それらの組合せに対応する量的変数(上図「マンガ作者数」のシェア)を、二つ目の 位置 スケールで表現する手法です。

積上げ棒グラフと類似した手法ですが、以下が異なります:

  • 量的変数として表現できるのは、基本的に全体に対する割合のみ

  • 積上げ棒グラフの「棒」に相当する部分の 太さ (上図「位置③」)が一定ではない。棒の太さは、一つ目の質的変数の全ての水準の和に対する当該水準の割合を表現している

これにより、積上げ棒グラフで内訳を表現する際の課題だった「異なる水準間での比較ができない」を解決することができます。 原理的には長方形同士の 面積 を比較することで、例えば1970年代の週刊少年マガジン2010年代の週刊少年チャンピオンのマンガ作者数を直接比較できます。

Plotlyではモザイクプロットを作成する簡易な方法が存在しません。他の手法と一貫性を保つため、本書ではplotly.expressライクな関数であるcreate_mosaicplotを定義します。

# create_mosaicplot関数を使用して、データフレームからモザイクプロットを作成
# 'df'にはプロットするデータを含むデータフレームを、'x'と'y'にはモザイクプロットのx軸とy軸に使用するカラム名を渡す
# 'color'は各セグメントの色を決定する基準となるカラム名で、これに基づいて色が割り当てられる
# 'width'と'text'はそれぞれセグメントの幅とセグメント上に表示されるテキストを定義するカラム名
# 作成したプロットは'fig'という変数に保存され、この'fig'を返り値として返す
fig = create_mosaicplot(
    df, x="col_x", y="col_y", color="col_color", width="col_width", text="col_text"
)

関数の定義は、こちらを参照ください。

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 効率的なループを実行するためのイテレータビルディングブロックを提供
# これにより、データのコンビネーションや順列などを簡潔に表現できる
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# typingモジュールからListのインポート
# 型ヒントとして利用
from typing import List

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsのインポート
# goという名前で参照可能
# 低レベルのインターフェイスを提供し、より詳細なカスタマイズが可能
import plotly.graph_objects as go

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")

# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "mosaic"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "mosaic"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "mosaic"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ作品とマンガ作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# アニメ作品と原作者の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"

# アニメ各話に関するファイル
FN_AE = "an_ae.csv"

# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# 可視化に関する設定値の定義

# 「年代」の集計単位
UNIT_YEARS = 10
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide code cell content
# 質的変数の描画用のカラースケールの定義

# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
    "#000000",  # 黒 (Black)
    "#E69F00",  # 橙 (Orange)
    "#56B4E9",  # 薄青 (Sky Blue)
    "#009E73",  # 青緑 (Bluish Green)
    "#F0E442",  # 黄色 (Yellow)
    "#0072B2",  # 青 (Blue)
    "#D55E00",  # 赤紫 (Vermilion)
    "#CC79A7",  # 紫 (Reddish Purple)
]

関数#

以下では、本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = UNIT_YEARS, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトはUNIT_YEARS
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def create_mosaicplot(
    df: pd.DataFrame,
    x: str,
    y: str,
    color: str,
    width: str,
    text: str,
    color_discrete_sequence: List[str] = OKABE_ITO,
) -> go.Figure:
    """
    指定されたDataFrameを元にモザイクプロットを作成する関数

    Parameters
    ----------
    df : pd.DataFrame
        プロットに使用するデータが含まれるDataFrame
    x : str
        x軸に表示するデータのカラム名
    y : str
        y軸に表示するデータのカラム名
    color : str
        グループ分けの基準となるデータのカラム名
    width : str
        各バーの幅を表すデータのカラム名
    text : str
        各バーに表示するテキストのデータのカラム名
    color_discrete_sequence : List[str], optional
        使用する色のリスト デフォルトはOKABE_ITOのカラーパレット

    Returns
    -------
    go.Figure
        作成されたモザイクプロットのFigureオブジェクト
    """

    # 空のFigureオブジェクトを作成
    fig = go.Figure()

    # color列に登場するユニークな要素に対し、色をマッピング
    unique_keys = df[color].unique()
    color_map = {
        name: color for name, color in zip(unique_keys, color_discrete_sequence)
    }

    # color列のユニークな要素ごとにDataFrameをフィルタリング
    for i, name in enumerate(unique_keys):
        df_tmp = df[df[color] == name].reset_index(drop=True)
        # 幅をwidth列から抽出
        widths = df_tmp[width]

        # バーの位置を計算し、プロットに追加
        # 幅が変わるようxの値を調整
        fig.add_trace(
            go.Bar(
                name=name,
                x=df_tmp[width].cumsum() - widths,
                y=df_tmp[y],
                text=df_tmp[text],
                width=widths,
                offset=0,
                marker_color=color_map[name],
            )
        )

        # 最初の要素を用いて、X軸ラベルの設定値を作成
        if i == 0:
            # 各「棒」の中央に配置されるように座標を計算
            tickvals = df_tmp[width].cumsum() - df_tmp[width] / 2
            ticktext = df_tmp[x].unique()
            # x軸の表示範囲を決定するために利用
            x_max = df_tmp[width].sum()

    # x軸の目盛りの位置、テキスト、表示範囲を設定
    # 「棒」の太さの合計値を1としたとき、左右に0.1ずつ余白が残るように調整
    fig.update_xaxes(
        tickvals=tickvals, ticktext=ticktext, title=x, range=[-x_max * 0.1, x_max * 1.1]
    )

    # y軸のタイトルを設定
    fig.update_yaxes(title=y)

    # プロットのレイアウトを設定、凡例タイトルも指定
    fig.update_layout(barmode="stack", legend_title=color) 

    return fig
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

マンガデータ#

雑誌ごとのマンガ作者数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
df_ce = pd.read_csv(DIR_CM / FN_CE)

重複してカウントされないよう、複数のマンガ雑誌に掲載経験のあるマンガ作者を可視化対象から除外します。

Hide code cell content
# df_ceに年代情報を追加するための関数add_years_to_dfを適用
df_ce = add_years_to_df(df_ce)

# yearsとccidのユニークな組み合わせを持つデータフレームを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てている
df_cc_years = df_ce.drop_duplicates(subset=["years", "ccid"], ignore_index=True)[
    ["ccid", "years"]
]
Hide code cell content
# df_cc_crtとdf_cc_yearsをccidを基準にして右結合(right join)し、df_mergeを作成
# これにより、df_cc_crtのデータにdf_cc_yearsのyears情報が組み合わされる
df_merge = pd.merge(df_cc_crt, df_cc_years, on="ccid", how="right")

# df_mergeをマンガ雑誌名とyearsごとにグループ化し、作者のユニーク数を集計
# nunique()を使用して各グループ内のユニークな作者数をカウント
df_cm = (
    df_merge.groupby(["mcname", "years"])["crtid"].nunique().reset_index(name="n_crt")
)

# years別の比率を計算するための一時的なDataFrameを作成し、マージ
df_tmp = df_cm.groupby("years")["n_crt"].sum().reset_index(name="years_total")
df_cm = pd.merge(df_cm, df_tmp, how="left", on="years")

# years別のマンガ作者数のシェアを計算
df_cm["ratio"] = df_cm["n_crt"] / df_cm["years_total"]
# 可視化した際の補足情報として、小数点以下2桁で丸めたシェアを計算
df_cm["text"] = df_cm["ratio"].apply(lambda x: f"{x: .2}")

# 列名をわかりやすい名前に変更
df_cm = df_cm.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "years": "年代",
        "n_crt": "マンガ作者数",
        "ratio": "マンガ作者数のシェア",
    }
)

円グラフでの分析結果から、マンガ作者数が極端に少ない年代はないことがわかっています。 そこで、今回も全ての年代を可視化対象とします。

Hide code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
マンガ雑誌名 年代 マンガ作者数 years_total マンガ作者数のシェア text
0 週刊少年サンデー 1970 197 866 0.227483 0.23
1 週刊少年サンデー 1980 208 886 0.234763 0.23
2 週刊少年サンデー 1990 182 768 0.236979 0.24
3 週刊少年サンデー 2000 181 879 0.205916 0.21
4 週刊少年サンデー 2010 191 946 0.201903 0.2
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/07/mosaic/cm.csv'.
Hide code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_cmから、x軸として年代列を、y軸としてマンガ作者のシェア列を指定
# colorとしてマンガ雑誌名列を指定することで、マンガ雑誌に応じて色分け
# widthでyears_totalの数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(df_cm, x="年代", y="マンガ作者数のシェア", color="マンガ雑誌名", width="years_total", text="text")

# モザイクプロットを表示
show_fig(fig)

上図は、マンガ雑誌別のマンガ作者数のシェアを、年代別に表現したモザイクプロットです。 各年代の合計マンガ作者数にそれほど差がないため棒グラフと印象は変わりませんが、実際には年代ごとに棒の幅が異なっています。

モザイクプロットの恩恵を受ける例の一つは、年代内のシェアとしては同様の値だが、年代間でスケールが異なるため絶対量には差があるような数量の比較です。 例えば、上図では1980年代、1990年代、そして2010年代の週刊少年チャンピオンのシェアが全て0.27と表現されていますが、絶対量はそれぞれ異なります。

Hide code cell content
# 週刊少年チャンピオンのデータを抽出
df_cm[df_cm["マンガ雑誌名"]=="週刊少年チャンピオン"]
マンガ雑誌名 年代 マンガ作者数 years_total マンガ作者数のシェア text
10 週刊少年チャンピオン 1970 182 866 0.210162 0.21
11 週刊少年チャンピオン 1980 239 886 0.269752 0.27
12 週刊少年チャンピオン 1990 208 768 0.270833 0.27
13 週刊少年チャンピオン 2000 265 879 0.301479 0.3
14 週刊少年チャンピオン 2010 256 946 0.270613 0.27

1990年のマンガ作者数は208人であるのに対し、2010年のそれは約 1.23倍256人です。 しかし、上図から ひと目で それを判断するのは至難の業です。

なお、円グラフの例と同様、これまでの可視化は全てある年代・あるマンガ雑誌に 掲載実績のある マンガ作者の内訳であり、 デビューした マンガ作者の内訳ではないことにご注意ください。

アニメデータ#

本データ中の声優の男女比[1]を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ac_act = pd.read_csv(DIR_AN / FN_AC_ACT)
df_ae = pd.read_csv(DIR_AN / FN_AE)

重複してカウントされないよう、本データ中の全ての声優が一つのgenderと紐づいていることを確認します。

Hide code cell content
# 各crtidに対して、関連するgenderのユニークな数を数える
# groupbyでcrtidごとにグループ化し、nuniqueメソッドで各グループのgenderのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各crtidが1つのgenderにのみ紐づいていること)を確認
assert all(df_ac_act.groupby("actid")["gender"].nunique() == 1)

AssertionErrorが出なかったため、このまま分析を進めます。

Hide code cell content
# df_aeに年代情報を追加するための関数add_years_to_dfを適用
# unit_years=5を指定することで、5年単位の年代情報を追加する
df_ae = add_years_to_df(df_ae, unit_years=5)

# acidとyearsのユニークな組み合わせを持つDataFrameを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てる
df_ac_years = df_ae.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
    ["acid", "years"]
]
Hide code cell content
# df_ac_actとdf_ac_yearsをacidを基準にして左結合(left join)し、df_mergeを作成
# これにより、df_ac_actのデータにdf_ac_yearsの年代情報が組み合わされる
df_merge = pd.merge(df_ac_act, df_ac_years, on="acid", how="left")

# df_mergeを性別と年代ごとにグループ化し、声優のユニーク数を集計
# nunique()を使用して各グループ内のユニークな声優数をカウント
df_an = (
    df_merge.groupby(["gender", "years"])["acid"].nunique().reset_index(name="n_act")
)

# 列名をわかりやすい名前に変更
df_an = df_an.rename(columns={"gender": "性別", "years": "年代", "n_act": "声優数"})

円グラフの分析結果に基づき、今回は2000年以降を可視化対象とします。

Hide code cell content
# 2000年代以降を可視化対象とする
df_an = df_an[df_an["年代"].astype(int) >= 2000].reset_index(drop=True)
Hide code cell content
# 年代ごとの合計声優数を格納する辞書を作成
year2nact = df_an.groupby("年代")["声優数"].sum().to_dict()

# df_anに合計声優数という列を追加し、year2nactから値を取得して格納
df_an["合計声優数"] = df_an["年代"].map(year2nact)

# df_anに内訳列を追加。声優数 / 合計声優数で算出
df_an["声優数のシェア"] = df_an["声優数"] / df_an["合計声優数"]

# df_anにテキスト表示用の列を追加
df_an["text"] = df_an["声優数のシェア"].apply(lambda x: f"{x:.2f}")
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
性別 年代 声優数 合計声優数 声優数のシェア text
0 female 2000 561 1113 0.504043 0.50
1 female 2005 844 1695 0.497935 0.50
2 female 2010 856 1654 0.517533 0.52
3 female 2015 597 1149 0.519582 0.52
4 male 2000 552 1113 0.495957 0.50
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/07/mosaic/an.csv'.
Hide code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_anから、x軸として年代列を、y軸として声優数のシェア列を指定
# colorとして性別列を指定することで、性別に応じて色分け
# widthで合計声優数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(df_an, x="年代", y="声優数のシェア", color="性別", width="合計声優数", text="text")

# モザイクプロットを表示
show_fig(fig)

上図は、性別ごとの声優数のシェアを年代別に表現したモザイクプロットです。 2000年から2015年代まで、基本的にシェアは拮抗していますが、それぞれの年代で分母が異なっていたことがわかります。

積上げ密度プロットとの違いを強調するために、年代の粒度を1年にして再度可視化してみましょう。

Hide code cell content
# df_aeに年代情報を追加するための関数add_years_to_dfを適用
# unit_years=1を指定することで、1年単位の年代情報を追加する
df_ae2 = add_years_to_df(df_ae, unit_years=1)

# acidとyearsのユニークな組み合わせを持つDataFrameを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てる
df_ac_years2 = df_ae2.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
    ["acid", "years"]
]
Hide code cell content
# df_ac_actとdf_ac_years2をacidを基準にして左結合(left join)し、df_mergeを作成
# これにより、df_ac_actのデータにdf_ac_yearsの年代情報が組み合わされる
df_merge2 = pd.merge(df_ac_act, df_ac_years2, on="acid", how="left")

# df_merge2を性別と年代ごとにグループ化し、声優のユニーク数を集計
# nunique()を使用して各グループ内のユニークな声優数をカウント
df_an2 = (
    df_merge2.groupby(["gender", "years"])["acid"].nunique().reset_index(name="n_act")
)

# 列名をわかりやすい名前に変更
df_an2 = df_an2.rename(columns={"gender": "性別", "years": "年代", "n_act": "声優数"})
Hide code cell content
# 2000年代以降を可視化対象とする
df_an2 = df_an2[df_an2["年代"].astype(int) >= 2000].reset_index(drop=True)
Hide code cell content
# 年代ごとの合計声優数を格納する辞書を作成
year2nact2 = df_an2.groupby("年代")["声優数"].sum().to_dict()

# df_an2に合計声優数という列を追加し、year2nact2から値を取得して格納
df_an2["合計声優数"] = df_an2["年代"].map(year2nact2)

# df_an2に内訳列を追加。声優数 / 合計声優数で算出
df_an2["声優数のシェア"] = df_an2["声優数"] / df_an2["合計声優数"]

# df_an2にテキスト表示用の列を追加
df_an2["text"] = df_an2["声優数のシェア"].apply(lambda x: f"{x:.2f}")
Hide code cell content
# 可視化対象のDataFrameを確認
df_an2.head()
性別 年代 声優数 合計声優数 声優数のシェア text
0 female 2000 127 252 0.503968 0.50
1 female 2001 145 287 0.505226 0.51
2 female 2002 147 288 0.510417 0.51
3 female 2003 173 342 0.505848 0.51
4 female 2004 202 407 0.496314 0.50
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../data/an/output/07/mosaic/an2.csv'.
Hide code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_an2から、x軸として年代列を、y軸として声優数のシェア列を指定
# colorとして性別列を指定することで、性別に応じて色分け
# widthで合計声優数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(df_an2, x="年代", y="声優数のシェア", color="性別", width="合計声優数", text="text")

# モザイクプロットを表示
show_fig(fig)

積上げ密度プロットによる結果と異なり、 モザイクプロットはX軸の間隔が一定ではない ことに注意しましょう。 つまり、密度プロットでは、X軸方向に一単位右に移動したとき水準の境界線が上下にどの程度動くかによって 単位時間の変化率 を目測することが可能ですが、モザイクプロットでは不可能です。 言い換えると、接線の傾きが微分値と対応していないということになります。

これは可視化表現の作成者にとっては当たり前の事実ですが、受け手にとってはそうではありません。 特に年代等、時間経過を表現する質的変数をX軸に確認した際、受け手は無意識のうちにX軸方向が等間隔な時間を表現していると仮定し、その傾きから変化率を目測しがちです。 誤解を防ぐため、モザイクプロットではなく積上げ密度プロットの利用を検討しましょう。

なお、円グラフの例と同様、以上の可視化は全てある年代・ある性別として 活動実績のある 声優の内訳であり、 デビューした 声優の内訳ではないことにご注意ください。

ゲームデータ#

発売曜日ごとのゲームパッケージ数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Hide code cell content
# df_pkg_pfに年代(5年刻み)を追加
df_pkg_pf = add_years_to_df(df_pkg_pf, unit_years=5)

# date列をdatetimeオブジェクトに変換して、曜日情報を新たな列としてdf_pkg_pfに追加
df_pkg_pf["weekday"] = pd.to_datetime(df_pkg_pf["date"]).dt.weekday

重複カウントを防ぐため、一つのゲームパッケージはかならず一つの発売曜日と紐づいていることを確認します。

Hide code cell content
# 各pkgidに対して、関連するweekdayのユニークな数を数える
# groupbyでpkgidごとにグループ化し、nuniqueメソッドで各グループのweekdayのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各pkgidが1つのweekdayにのみ紐づいていること)を確認
assert all(df_pkg_pf.groupby("pkgid")["weekday"].nunique()==1)

AssertionErrorが出なかったので、このまま分析を進めます。

Hide code cell content
# years、weekdayごとにpkgidのユニーク数を集計し、n_pkg列として追加
df_gm = (
    df_pkg_pf.groupby(["years", "weekday"])["pkgid"].nunique().reset_index(name="n_pkg")
)

# 数値で表されている曜日を文字列にマッピング
df_gm["yobi"] = df_gm["weekday"].map(WEEKDAY2YOBI)

# 可視化用に列名をリネーム
df_gm = df_gm.rename(columns={"years": "発売年代", "yobi": "発売曜日", "n_pkg": "パッケージ数"})

円グラフでの分析結果から、今回は1990年代以降を可視化対象とします。

Hide code cell content
# 1990年以降のデータに絞る
df_gm = df_gm[df_gm["発売年代"].astype(int) >= 1990].reset_index(drop=True)
Hide code cell content
# 年代別の合計ゲームパッケージ数を集計し、辞書として保存
year2npkg = df_gm.groupby("発売年代")["パッケージ数"].sum().to_dict()

# 年代をキーに、year2npkgで合計パッケージ数をマッピング
df_gm["合計パッケージ数"] = df_gm["発売年代"].map(year2npkg)

# 年ごとのシェアを計算
df_gm["パッケージ数のシェア"] = df_gm["パッケージ数"] / df_gm["合計パッケージ数"]

# 表示用に小数点以下二桁で丸めた列を追加
df_gm["text"] = df_gm["パッケージ数のシェア"].apply(lambda x: f"{x:.2}")
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
発売年代 weekday パッケージ数 発売曜日 合計パッケージ数 パッケージ数のシェア text
0 1990 0 34 2074 0.016393 0.016
1 1990 1 81 2074 0.039055 0.039
2 1990 2 66 2074 0.031823 0.032
3 1990 3 122 2074 0.058824 0.059
4 1990 4 1559 2074 0.751688 0.75
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/07/mosaic/gm.csv'.
Hide code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_gmから、x軸として年代列を、y軸としてパッケージ数のシェア列を指定
# colorとして曜日列を指定することで、曜日に応じて色分け
# widthで合計パッケージ数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(df_gm, x="発売年代", y="パッケージ数のシェア", color="発売曜日", width="合計パッケージ数", text="text")

# モザイクプロットを表示
show_fig(fig)

上図は、ゲームパッケージの発売曜日のシェアを年代別に表現したモザイクプロットです。

アニメデータで言及した注意点を思い出しましょう。 1990年代から1995年代にかけて、非常に急激に木曜のシェアが伸びているような印象を持ちますが、これを鵜呑みにしてはいけません。 なぜなら、1990年代は比較的ゲームパッケージの発売数が少なかったため1990年代と1995年代の間隔が狭くなり、真実以上に傾きが急に見えているためです。